import UIKit
import MobileCoreServices
import AVKit
import Photos
import RxSwift
import RxCocoa

class ConversationViewController: UIViewController, StatusBarStyleSwitchableViewController {
    
    @IBOutlet weak var galleryWrapperView: UIView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var connectionHintView: ConnectionHintView!
    @IBOutlet weak var tableView: ConversationTableView!
    @IBOutlet weak var announcementButton: UIButton!
    @IBOutlet weak var scrollToBottomWrapperView: UIView!
    @IBOutlet weak var scrollToBottomButton: UIButton!
    @IBOutlet weak var unreadBadgeLabel: UILabel!
    @IBOutlet weak var bottomOutsideWrapperView: UIView!
    @IBOutlet weak var inputWrapperView: UIView!
    @IBOutlet weak var moreButton: UIButton!
    @IBOutlet weak var botButton: UIButton!
    @IBOutlet weak var inputTextView: InputTextView!
    @IBOutlet weak var sendButton: UIButton!
    @IBOutlet weak var toggleStickerPanelSizeButton: UIButton!
    @IBOutlet weak var stickerKeyboardSwitcherButton: UIButton!
    @IBOutlet weak var avatarImageView: AvatarImageView!
    @IBOutlet weak var participantsLabel: UILabel!
    @IBOutlet weak var unblockButton: StateResponsiveButton!
    @IBOutlet weak var deleteConversationButton: StateResponsiveButton!
    @IBOutlet weak var stickerPanelContainerView: UIView!
    @IBOutlet weak var moreMenuContainerView: UIView!
    @IBOutlet weak var dismissPanelsButton: UIButton!
    @IBOutlet weak var loadingView: UIActivityIndicatorView!
    @IBOutlet weak var titleStackView: UIStackView!
    @IBOutlet weak var audioInputContainerView: UIView!
    @IBOutlet weak var quotePreviewView: QuotePreviewView!
    
    @IBOutlet weak var scrollToBottomWrapperHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var inputTextViewHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var inputTextViewLeadingShrinkConstraint: NSLayoutConstraint!
    @IBOutlet weak var inputTextViewLeadingExpandedConstraint: NSLayoutConstraint!
    @IBOutlet weak var inputWrapperBottomConstraint: NSLayoutConstraint!
    @IBOutlet weak var stickerPanelContainerHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var moreMenuHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var moreMenuTopConstraint: NSLayoutConstraint!
    @IBOutlet weak var audioInputContainerWidthConstraint: NSLayoutConstraint!
    @IBOutlet weak var quoteViewHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var quoteViewHiddenConstraint: NSLayoutConstraint!
    @IBOutlet weak var quoteViewShowConstraint: NSLayoutConstraint!
    
    static var positions = [String: Position]()
    
    private let disposeBag = DisposeBag()
    
    var dataSource: ConversationDataSource!
    var conversationId: String {
        return dataSource.conversationId
    }
    var statusBarStyle = UIStatusBarStyle.default {
        didSet {
            setNeedsStatusBarAppearanceUpdate()
        }
    }
    
    private let maxInputRow = 6
    private let showScrollToBottomButtonThreshold: CGFloat = 150
    private let loadMoreMessageThreshold = 20
    private let animationDuration: TimeInterval = 0.25
    private let minReasonableKeyboardHeight: CGFloat = 271
    private let moreMenuSegueId = "MoreMenuSegueId"
    private let audioInputSegueId = "AudioInputSegueId"
    
    private var ownerUser: UserItem?
    private var participants = [Participant]()
    private var role = ""
    private var asset: AssetItem?
    private var quoteMessageId: String?
    private var quotingMessageId: String?
    private var isShowingMenu = false
    private var isShowingStickerPanel = false
    private var isAppearanceAnimating = true
    private var isStickerPanelMax = false
    private var hideStatusBar = false
    private var inputWrapperShouldFollowKeyboardPosition = true
    private var isShowingQuotePreviewView: Bool {
        return quoteMessageId != nil
    }
    
    private var tapRecognizer: UITapGestureRecognizer!
    private var reportRecognizer: UILongPressGestureRecognizer!
    private var moreMenuViewController: ConversationMoreMenuViewController?
    private var audioInputViewController: AudioInputViewController?
    private var previewDocumentController: UIDocumentInteractionController?
    private var userBot: App?
    
    private(set) lazy var imagePickerController = ImagePickerController(initialCameraPosition: .rear, cropImageAfterPicked: false, parent: self)
    private lazy var userWindow = UserWindow.instance()
    
    private lazy var lastInputWrapperBottomConstant = bottomSafeAreaInset
    private lazy var lastKeyboardHeight = minReasonableKeyboardHeight
    
    private lazy var stickerPanelViewController: StickerPanelViewController = {
        let controller = StickerPanelViewController.instance()
        addChildViewController(controller)
        stickerPanelContainerView.addSubview(controller.view)
        controller.view.snp.makeConstraints({ (make) in
            make.edges.equalToSuperview()
        })
        controller.didMove(toParentViewController: self)
        stickerPanelContainerView.layoutIfNeeded()
        return controller
    }()
    private lazy var galleryViewController: GalleryViewController = {
        let controller = GalleryViewController.instance(conversationId: conversationId)
        controller.delegate = self
        addChildViewController(controller)
        galleryWrapperView.addSubview(controller.view)
        controller.view.snp.makeConstraints({ (make) in
            make.edges.equalToSuperview()
        })
        controller.didMove(toParentViewController: self)
        return controller
    }()
    private lazy var strangerTipsView: StrangerTipsView = {
        let view = StrangerTipsView()
        view.frame.size.height = StrangerTipsView.height
        view.blockButton.addTarget(self, action: #selector(blockAction(_:)), for: .touchUpInside)
        view.addContactButton.addTarget(self, action: #selector(addContactAction(_:)), for: .touchUpInside)
        return view
    }()

    private var bottomSafeAreaInset: CGFloat {
        if #available(iOS 11.0, *) {
            return view.safeAreaInsets.bottom
        } else {
            return 0
        }
    }

    private var stickerPanelFullsizedHeight: CGFloat {
        if #available(iOS 11.0, *) {
            return view.frame.height - 56 - max(view.safeAreaInsets.top, 20) - view.safeAreaInsets.bottom - 55
        } else {
            return view.frame.height - 56 - 20 - 55
        }
    }
    
    private var isShowingMoreMenu: Bool {
        return moreMenuTopConstraint.constant > 0.1
    }
    
    private var unreadBadgeValue: Int = 0 {
        didSet {
            guard unreadBadgeValue != oldValue else {
                return
            }
            unreadBadgeLabel.isHidden = unreadBadgeValue <= 0
            unreadBadgeLabel.text = unreadBadgeValue <= 99 ? String(unreadBadgeValue) : "99+"
        }
    }
    
    override var prefersStatusBarHidden: Bool {
        return hideStatusBar
    }
    
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return statusBarStyle
    }
    
    // MARK: - Life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        showLoading()
        if let swipeBackRecognizer = navigationController?.interactivePopGestureRecognizer {
            tableView.gestureRecognizers?.forEach {
                $0.require(toFail: swipeBackRecognizer)
            }
        }
        if let conversation = dataSource?.conversation {
            titleLabel.text = conversation.getConversationName()
        } else if let ownerUser = ownerUser {
            titleLabel.text = ownerUser.fullName
        }

        reportRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(showReportMenuAction))
        reportRecognizer.minimumPressDuration = 2
        titleLabel.isUserInteractionEnabled = true
        titleLabel.addGestureRecognizer(reportRecognizer)

        audioInputContainerWidthConstraint.constant = 55
        quotePreviewView.dismissAction = { [weak self] in
            self?.setQuoteViewHidden(true)
        }
        tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
        tapRecognizer.delegate = self
        tableView.addGestureRecognizer(tapRecognizer)
        tableView.dataSource = self
        tableView.delegate = self
        tableView.actionDelegate = self
        tableView.viewController = self
        let bottomSafeAreaInset: CGFloat
        if #available(iOS 11.0, *) {
            bottomSafeAreaInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
        } else {
            view.layoutIfNeeded()
            bottomSafeAreaInset = 0
        }
        tableView.contentInset.bottom = inputWrapperView.frame.height + bottomSafeAreaInset + MessageViewModel.bottomSeparatorHeight
        tableView.scrollIndicatorInsets.bottom = inputWrapperView.frame.height
        inputTextView.delegate = self
        inputTextView.layer.cornerRadius = inputTextViewHeightConstraint.constant / 2
        connectionHintView.delegate = self
        loadStickerAndAsset()

        dataSource.ownerUser = ownerUser
        dataSource.tableView = tableView
        dataSource.initData {
            self.updateAccessoryButtons(animated: false)
        }
        updateMoreMenuFixedJobs()
        updateMoreMenuApps()
        updateStrangerTipsView()
        updateBottomView()
        inputWrapperView.isHidden = false
        loadDraft()
        updateNavigationBar()
        reloadParticipants()
        NotificationCenter.default.addObserver(self, selector: #selector(conversationDidChange(_:)), name: .ConversationDidChange, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(userDidChange(_:)), name: .UserDidChange, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: .UIKeyboardWillChangeFrame, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(menuControllerDidShowMenu(_:)), name: .UIMenuControllerDidShowMenu, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(menuControllerDidHideMenu(_:)), name: .UIMenuControllerDidHideMenu, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(participantDidChange(_:)), name: .ParticipantDidChange, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(assetsDidChange(_:)), name: .AssetsDidChange, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(didAddedMessagesOutsideVisibleBounds(_:)), name: Notification.Name.ConversationDataSource.DidAddedMessagesOutsideVisibleBounds, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(applicationWillTerminate(_:)), name: .UIApplicationWillTerminate, object: nil)
        hideLoading()
    }

    @objc func showReportMenuAction() {
        guard !self.conversationId.isEmpty else {
            return
        }

        let conversationId = self.conversationId
        let alc = UIAlertController(title: Localized.REPORT_TITLE, message: nil, preferredStyle: .actionSheet)
        alc.addAction(UIAlertAction(title: Localized.REPORT_BUTTON, style: .default, handler: { [weak self](_) in
            self?.reportAction(conversationId: conversationId)
        }))
        alc.addAction(UIAlertAction(title: Localized.DIALOG_BUTTON_CANCEL, style: .cancel, handler: nil))
        self.present(alc, animated: true, completion: nil)
    }

    private func reportAction(conversationId: String) {
        
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == moreMenuSegueId, let destination = segue.destination as? ConversationMoreMenuViewController {
            moreMenuViewController = destination
        } else if segue.identifier == audioInputSegueId, let destination = segue.destination as? AudioInputViewController {
            audioInputViewController = destination
        }
    }
    
    @available(iOS 11.0, *)
    override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        if inputWrapperBottomConstraint.constant == 0 {
            inputWrapperBottomConstraint.constant = bottomSafeAreaInset
        }
    }
    
    override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) {
        super.preferredContentSizeDidChange(forChildContentContainer: container)
        if (container as? UIViewController) == audioInputViewController {
            let isExpanding = container.preferredContentSize.width > audioInputContainerWidthConstraint.constant
            audioInputContainerWidthConstraint.constant = container.preferredContentSize.width
            if isExpanding {
                UIView.animate(withDuration: animationDuration, animations: {
                    self.view.layoutIfNeeded()
                })
            } else {
                view.layoutIfNeeded()
            }
        }
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        isAppearanceAnimating = true
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        isAppearanceAnimating = false
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        dismissMenu(animated: true)
        isAppearanceAnimating = true
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        saveDraft()
        MXNAudioPlayer.shared().stop(withAudioSessionDeactivated: true)
        if let visibleIndexPaths = tableView.indexPathsForVisibleRows {
            if let lastIndexPath = dataSource?.lastIndexPath, visibleIndexPaths.contains(lastIndexPath) {
                ConversationViewController.positions[conversationId] = nil
            } else {
                for indexPath in visibleIndexPaths {
                    guard let message = dataSource?.viewModel(for: indexPath)?.message, !message.isExtensionMessage else {
                        continue
                    }
                    let rect = tableView.rectForRow(at: indexPath)
                    let offset = tableView.contentOffset.y - rect.origin.y
                    ConversationViewController.positions[conversationId] = Position(messageId: message.messageId, offset: offset)
                    break
                }
            }
        }
        if parent == nil {
            dataSource?.cancelMessageProcessing()
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    // MARK: - Actions
    @IBAction func profileAction(_ sender: Any) {
        if let dataSource = dataSource, dataSource.category == .group {
            
            
        } else if let user = ownerUser {
            userWindow.bounds.size.width = view.bounds.width
            userWindow.updateUser(user: user).presentView()
        }
    }
    
    @IBAction func announcementAction(_ sender: Any) {

    }
    
    @IBAction func backAction(_ sender: Any) {
        navigationController?.popViewController(animated: true)
    }
    
    @IBAction func scrollToBottomAction(_ sender: Any) {
        unreadBadgeValue = 0
        if let quotingMessageId = quotingMessageId, let indexPath = dataSource?.indexPath(where: { $0.messageId == quotingMessageId }) {
            self.quotingMessageId = nil
            tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
            blinkCellBackground(at: indexPath)
        } else if let quotingMessageId = quotingMessageId {
            self.quotingMessageId = nil
            dataSource?.scrollToBottomAndReload(initialMessageId: quotingMessageId, completion: {
                if let indexPath = self.dataSource?.indexPath(where: { $0.messageId == quotingMessageId }) {
                    self.blinkCellBackground(at: indexPath)
                }
            })
        } else {
            dataSource?.scrollToFirstUnreadMessageOrBottom()
        }
    }
    
    @IBAction func moreAction(_ sender: Any) {
        var delay: TimeInterval = 0
        if isShowingStickerPanel {
            delay = animationDuration
            toggleStickerPanel(delay: 0)
        }
        if inputTextView.isFirstResponder {
            inputTextView.resignFirstResponder()
        }
        DispatchQueue.main.async {
            self.toggleMoreMenu(delay: delay)
        }
    }
    
    @IBAction func stickerKeyboardSwitchAction(_ sender: Any) {
        var delay: TimeInterval = 0
        if isShowingMoreMenu {
            toggleMoreMenu(delay: 0)
            delay = animationDuration
        }
        if isShowingStickerPanel {
            inputTextView.becomeFirstResponder()
        } else {
            inputWrapperShouldFollowKeyboardPosition = false
            inputTextView.resignFirstResponder()
            inputWrapperShouldFollowKeyboardPosition = true
            toggleStickerPanel(delay: delay)
        }
    }

    @IBAction func botAction(_ sender: Any) {
        guard let user = ownerUser, user.isBot, let app = self.userBot else {
            return
        }
        guard let url = URL(string: app.homeUri), !conversationId.isEmpty else {
            return
        }

        if isShowingMoreMenu {
            toggleMoreMenu(delay: 0)
        }
        if isShowingStickerPanel {
            toggleStickerPanel(delay: 0)
        }
        presentWebWindow(withURL: url)
    }
    
    @IBAction func toggleStickerPanelSizeAction(_ sender: Any) {
        let dismissButtonAlpha: CGFloat
        if isStickerPanelMax {
            stickerPanelContainerHeightConstraint.constant = lastKeyboardHeight
            dismissButtonAlpha = 0
        } else {
            stickerPanelContainerHeightConstraint.constant = stickerPanelFullsizedHeight
            dismissButtonAlpha = 0.3
        }
        inputWrapperBottomConstraint.constant = stickerPanelContainerHeightConstraint.constant
        isStickerPanelMax = !isStickerPanelMax
        toggleStickerPanelSizeButton.animationSwapImage(newImage: isStickerPanelMax ? #imageLiteral(resourceName: "ic_chat_panel_min") : #imageLiteral(resourceName: "ic_chat_panel_max"))
        UIView.animate(withDuration: animationDuration, animations: {
            self.dismissPanelsButton.alpha = dismissButtonAlpha
            self.view.layoutIfNeeded()
        })
    }
    
    @IBAction func sendTextMessageAction(_ sender: Any) {
        guard !trimmedMessageDraft.isEmpty else {
            return
        }
        
        dataSource?.sendMessage(type: .SIGNAL_TEXT, quoteMessageId: quoteMessageId , value: trimmedMessageDraft)
        inputTextView.text = ""
        textViewDidChange(inputTextView)
        setQuoteViewHidden(true)
        
    }
    
    @IBAction func dismissPanelsAction(_ sender: Any) {
        if isShowingStickerPanel && isStickerPanelMax {
            toggleStickerPanel(delay: 0)
        } else {
            toggleMoreMenu(delay: 0)
        }
    }

    @IBAction func deleteConversationAction(_ sender: Any) {
        guard !self.conversationId.isEmpty else {
            return
        }
        deleteConversationButton.isBusy = true
        let conversationId = self.conversationId
        DispatchQueue.global().async { [weak self] in

            NotificationCenter.default.postOnMain(name: .ConversationDidChange)
            DispatchQueue.main.async {
                self?.navigationController?.backToHome()
            }
        }
    }
    
    @objc func blockAction(_ sender: Any) {
        guard let userId = ownerUser?.userId else {
            return
        }
        strangerTipsView.blockButton.isBusy = true
        
    }
    
    @objc func unblockAction(_ sender: Any) {
        guard let user = ownerUser else {
            return
        }
        unblockButton.isBusy = true
        
    }
    
    @objc func addContactAction(_ sender: Any) {
        guard let user = ownerUser else {
            return
        }
        strangerTipsView.addContactButton.isBusy = true
        
    }
    
    @objc func tapAction(_ recognizer: UIGestureRecognizer) {
        if let audioInputViewController = audioInputViewController, audioInputViewController.isShowingLongPressHint {
            audioInputViewController.animateHideLongPressHint()
            return
        }
        guard !isShowingMenu else {
            dismissMenu(animated: true)
            return
        }
        guard !inputTextView.isFirstResponder else {
            inputTextView.resignFirstResponder()
            return
        }
        guard !isShowingStickerPanel else {
            toggleStickerPanel(delay: 0)
            return
        }
        guard let indexPath = tableView.indexPathForRow(at: recognizer.location(in: tableView)), let cell = tableView.cellForRow(at: indexPath), let viewModel = dataSource?.viewModel(for: indexPath) else {
            return
        }
        let message = viewModel.message
        if message.category.hasSuffix("_IMAGE") || message.category.hasSuffix("_VIDEO") {
            guard message.mediaStatus == MediaStatus.DONE.rawValue, let cell = cell as? PhotoRepresentableMessageCell, cell.contentImageView.frame.contains(recognizer.location(in: cell)), let item = GalleryItem(message: message) else {
                return
            }
            MXNAudioPlayer.shared().stop(withAudioSessionDeactivated: true)
            view.bringSubview(toFront: galleryWrapperView)
            galleryViewController.show(item: item)
        } else if message.category.hasSuffix("_AUDIO") {
            guard message.mediaStatus == MediaStatus.DONE.rawValue, let cell = cell as? AudioMessageCell else {
                return
            }
            let cellIsPlaying = cell.isPlaying
            MXNAudioPlayer.shared().stop(withAudioSessionDeactivated: cellIsPlaying)
            if !cellIsPlaying {
                cell.isPlaying = true
                if let mediaUrl = viewModel.message.mediaUrl {
                    let path = MixinFile.url(ofChatDirectory: .audios, filename: mediaUrl).path
                    MXNAudioPlayer.shared().playFile(atPath: path) { [weak cell] (success, error) in
                        if let error = error as? MXNAudioPlayerError, error == .cancelled {
                            DispatchQueue.main.async {
                                cell?.isPlaying = false
                            }
                        } else if let error = error {
                            UIApplication.trackError("ConversationViewController", action: "Play audio", userInfo: ["error": error])
                        }
                    }
                }
            }
        } else if message.category.hasSuffix("_DATA") {
            guard let viewModel = viewModel as? DataMessageViewModel, let cell = cell as? DataMessageCell else {
                return
            }
            if viewModel.mediaStatus == MediaStatus.DONE.rawValue {
                openDocumentAction(message: message)
            } else {
                attachmentLoadingCellDidSelectNetworkOperation(cell)
            }
        } else if message.category.hasSuffix("_CONTACT") {
            guard let cell = cell as? ContactMessageCell, cell.contentFrame.contains(recognizer.location(in: cell)) else {
                return
            }
            guard let shareUserId = message.sharedUserId else {
                return
            }

        } else if message.category == MessageCategory.EXT_ENCRYPTION.rawValue {
            guard let cell = cell as? SystemMessageCell, cell.contentFrame.contains(recognizer.location(in: cell)) else {
                return
            }
            open(url: .aboutEncryption)
        } else if message.category == MessageCategory.SYSTEM_ACCOUNT_SNAPSHOT.rawValue {
            
        } else if message.category.hasSuffix("_TEXT") {
            guard let cell = cell as? QuoteTextMessageCell, cell.quoteBackgroundImageView.frame.contains(recognizer.location(in: cell)), let quoteMessageId = viewModel.message.quoteMessageId else {
                return
            }
            
            if let indexPath = dataSource?.indexPath(where: { $0.messageId == quoteMessageId }) {
                quotingMessageId = message.messageId
                tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
                blinkCellBackground(at: indexPath)
            }
        }
    }
    
    // MARK: - Callbacks
    @objc func conversationDidChange(_ sender: Notification) {
        guard let change = sender.object as? ConversationChange, change.conversationId == conversationId else {
            return
        }
        switch change.action {
        case let .updateGroupIcon(iconUrl):
            avatarImageView?.setGroupImage(with: iconUrl, conversationId: conversationId)
        case .update:
            hideLoading()
        case let .updateConversation(conversation):
            if !conversation.name.isEmpty {
                titleLabel.text = conversation.name
                dataSource?.conversation.name = conversation.name
            }
            dataSource?.conversation.announcement = conversation.announcement

            hideLoading()
        case .startedUpdateConversation:
            showLoading()
        default:
            break
        }
    }
    
    @objc func userDidChange(_ sender: Notification) {
        
    }
    
    @objc func keyboardWillChangeFrame(_ notification: Notification) {
        guard !isAppearanceAnimating && inputWrapperShouldFollowKeyboardPosition else {
            return
        }
        guard presentedViewController == nil else {
            return
        }
        guard let endFrame = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
            return
        }
        lastKeyboardHeight = max(minReasonableKeyboardHeight, endFrame.height)
        let windowHeight = AppDelegate.current.window!.bounds.height
        inputWrapperBottomConstraint.constant = max(windowHeight - endFrame.origin.y, bottomSafeAreaInset)
        let inputWrapperDisplacement = lastInputWrapperBottomConstant - inputWrapperBottomConstraint.constant
        let keyboardIsMovingUp = inputWrapperBottomConstraint.constant > bottomSafeAreaInset
        if isShowingStickerPanel && keyboardIsMovingUp {
            isShowingStickerPanel = false
            toggleStickerPanelSizeButton.isHidden = true
            stickerKeyboardSwitcherButton.setImage(#imageLiteral(resourceName: "ic_chat_sticker"), for: .normal)
            stickerPanelContainerView.alpha = 0
        }
        if keyboardIsMovingUp {
            if inputTextView.hasText || isShowingQuotePreviewView {
                sendButton.isHidden = false
            } else {
                audioInputContainerView.isHidden = false
            }
        }
        if isShowingMoreMenu {
            toggleMoreMenu(delay: 0)
        }
        lastInputWrapperBottomConstant = inputWrapperBottomConstraint.constant
        view.layoutIfNeeded()
        let contentOffsetY = tableView.contentOffset.y
        updateBottomInset()
        if !isShowingQuotePreviewView {
            tableView.setContentOffsetYSafely(contentOffsetY - inputWrapperDisplacement)
        }
    }
    
    @objc func menuControllerDidShowMenu(_ notification: Notification) {
        isShowingMenu = true
    }
    
    @objc func menuControllerDidHideMenu(_ notification: Notification) {
        isShowingMenu = false
        inputTextView.overrideNext = nil
    }
    
    @objc func participantDidChange(_ notification: Notification) {
        guard let conversationId = notification.object as? String, conversationId == self.conversationId else {
            return
        }
        updateMoreMenuApps()
        reloadParticipants()
    }
    
    @objc func assetsDidChange(_ notification: Notification) {
        
    }
    
    @objc func didAddedMessagesOutsideVisibleBounds(_ notification: Notification) {
        guard let count = notification.object as? Int else {
            return
        }
        unreadBadgeValue += count
    }
    
    @objc func applicationWillTerminate(_ notification: Notification) {
        saveDraft()
    }
    
    @objc func dismissQuoteView(_ sender: Any) {
        setQuoteViewHidden(true)
    }
    
    // MARK: - Interface
    func toggleMoreMenu(delay: TimeInterval) {
        let dismissButtonAlpha: CGFloat
        if isShowingMoreMenu {
            moreMenuTopConstraint.constant = 0
            dismissButtonAlpha = 0
        } else {
            moreMenuTopConstraint.constant = moreMenuViewController?.contentHeight ?? moreMenuHeightConstraint.constant
            dismissButtonAlpha = 0.3
        }
        UIView.animate(withDuration: animationDuration, delay: delay, options: [], animations: {
            UIView.setAnimationCurve(.overdamped)
            self.dismissPanelsButton.alpha = dismissButtonAlpha
            self.view.layoutIfNeeded()
        }, completion: nil)
    }
    
    func documentAction() {
        let vc = UIDocumentPickerViewController(documentTypes: ["public.item", "public.content"], in: .import)
        vc.delegate = self
        vc.modalPresentationStyle = .formSheet
        present(vc, animated: true, completion: nil)
    }
    
    func transferAction() {
        
    }

    func contactAction() {
        navigationController?.pushViewController(ConversationShareContactViewController.instance(ownerUser: ownerUser, conversationId: conversationId), animated: true)
    }

    func pickPhotoOrVideoAction() {
        PHPhotoLibrary.checkAuthorization { [weak self](authorized) in
            guard authorized, let weakSelf = self else {
                return
            }
            let picker = PhotoAssetPickerNavigationController.instance(pickerDelegate: weakSelf)
            weakSelf.present(picker, animated: true, completion: nil)
        }
    }

    func reduceStickerPanelHeightIfMaximized() {
        guard isStickerPanelMax else {
            return
        }
        toggleStickerPanelSizeAction(self)
    }
    
    func setInputWrapperHidden(_ hidden: Bool) {
        UIView.animate(withDuration: animationDuration) {
            self.inputWrapperView.alpha = hidden ? 0 : 1
        }
    }
    
    var conversation: ConversationItem!
    
    // MARK: - Class func
    class func instance(conversation: ConversationItem, highlight: ConversationDataSource.Highlight? = nil) -> ConversationViewController {
        let vc = Storyboard.chat.instantiateViewController(withIdentifier: "conversation") as! ConversationViewController
        let dataSource = ConversationDataSource(conversation: conversation, highlight: highlight)
        if dataSource.category == .contact {

        }
        vc.dataSource = dataSource
        vc.conversation = conversation
        return vc
    }
    
    class func instance(conversationItem: ConversationItem) -> ConversationViewController {
        let vc = Storyboard.chat.instantiateViewController(withIdentifier: "conversation") as! ConversationViewController
//        vc.ownerUser = ownerUser
        vc.dataSource = ConversationDataSource(conversation: conversationItem)
        return vc
    }
    
    class func instance() -> ConversationViewController {
        let vc = Storyboard.chat.instantiateViewController(withIdentifier: "conversation") as! ConversationViewController
        let dataSource = ConversationDataSource()
        vc.dataSource = dataSource
        return vc
    }
}

// MARK: - UIGestureRecognizerDelegate
extension ConversationViewController: UIGestureRecognizerDelegate {
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if isShowingMenu {
            return true
        }
        if let view = touch.view as? TextMessageLabel {
            return !view.canResponseTouch(at: touch.location(in: view))
        }
        return true
    }
    
}

// MARK: - UITextViewDelegate
extension ConversationViewController: UITextViewDelegate {
    
    func textViewDidChange(_ textView: UITextView) {
        guard let lineHeight = textView.font?.lineHeight else {
            return
        }
        let maxHeight = ceil(lineHeight * CGFloat(maxInputRow) + textView.textContainerInset.top + textView.textContainerInset.bottom)
        let contentSize = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: UILayoutFittingExpandedSize.height))
        inputTextView.isScrollEnabled = contentSize.height > maxHeight
        if !trimmedMessageDraft.isEmpty || isShowingQuotePreviewView {
            sendButton.isHidden = false
            audioInputContainerView.isHidden = true
            stickerKeyboardSwitcherButton.isHidden = true
        } else {
            sendButton.isHidden = true
            audioInputContainerView.isHidden = false
            stickerKeyboardSwitcherButton.isHidden = false
        }
        let newHeight = min(contentSize.height, maxHeight)
        let heightDifference = newHeight - inputTextViewHeightConstraint.constant
        if abs(heightDifference) > 0.1 {
            inputTextViewHeightConstraint.constant = newHeight
            let newContentOffset = tableView.contentOffset.y + heightDifference
            UIView.animate(withDuration: animationDuration, animations: {
                self.view.layoutIfNeeded()
                self.updateBottomInset()
                self.tableView.setContentOffsetYSafely(newContentOffset)
            })
        }
    }
    
}

// MARK: - ConnectionHintViewDelegate
extension ConversationViewController: ConnectionHintViewDelegate {
    
    func animateAlongsideConnectionHintView(_ view: ConnectionHintView, changingHeightWithDifference heightDifference: CGFloat) {
        tableView.contentOffset.y += heightDifference
    }
    
}

// MARK: - UITableViewDataSource
extension ConversationViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource?.viewModels(for: section)?.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let viewModel = dataSource?.viewModel(for: indexPath) else {
            return self.tableView.dequeueReusableCell(withReuseId: .unknown, for: indexPath)
        }
        let cell = self.tableView.dequeueReusableCell(withMessage: viewModel.message, for: indexPath)
        if let cell = cell as? MessageCell {
            UIView.performWithoutAnimation {
                cell.render(viewModel: viewModel)
            }
        }
        return cell
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return dataSource?.dates.count ?? 0
    }
    
}

// MARK: - ConversationTableViewActionDelegate
extension ConversationViewController: ConversationTableViewActionDelegate {
    
    func conversationTableViewCanBecomeFirstResponder(_ tableView: ConversationTableView) -> Bool {
        return !inputTextView.isFirstResponder
    }
    
    func conversationTableViewLongPressWillBegan(_ tableView: ConversationTableView) {
        inputTextView.overrideNext = tableView
    }
    
    func conversationTableView(_ tableView: ConversationTableView, hasActionsforIndexPath indexPath: IndexPath) -> Bool {
        guard let message = dataSource?.viewModel(for: indexPath)?.message else {
            return false
        }
        return !message.allowedActions.isEmpty
    }
    
    func conversationTableView(_ tableView: ConversationTableView, canPerformAction action: Selector, forIndexPath indexPath: IndexPath) -> Bool {
        guard let message = dataSource?.viewModel(for: indexPath)?.message else {
            return false
        }
        return message.allowedActions.contains(action)
    }
    
    func conversationTableView(_ tableView: ConversationTableView, didSelectAction action: ConversationTableView.Action, forIndexPath indexPath: IndexPath) {
        guard let viewModel = dataSource?.viewModel(for: indexPath) else {
            return
        }
        let message = viewModel.message
        switch action {
        case .copy:
            if message.category.hasSuffix("_TEXT") {
                UIPasteboard.general.string = message.content
            }
        case .delete:
            (viewModel as? AttachmentLoadingViewModel)?.cancelAttachmentLoading(markMediaStatusCancelled: false)
            dataSource?.queue.async {
                
            }
        case .forward:
            audioInputViewController?.cancelIfRecording()
            navigationController?.pushViewController(ForwardViewController.instance(message: message, ownerUser: ownerUser), animated: true)
        case .reply:
            audioInputViewController?.cancelIfRecording()
            quoteMessageId = message.messageId
            quotePreviewView.render(message: message, contentImageThumbnail: viewModel.thumbnail)
            setQuoteViewHidden(false)
            inputTextView?.becomeFirstResponder()
            let newTableViewHeight = tableView.frame.height - lastKeyboardHeight - inputWrapperView.frame.height
            let offsetY = tableView.rectForRow(at: indexPath).maxY - newTableViewHeight + quotePreviewView.frame.height
            if offsetY > 0 {
                UIView.animate(withDuration: 0.5, animations: {
                    UIView.setAnimationCurve(.overdamped)
                    tableView.contentOffset.y = offsetY
                })
            }
        case .add:
            if message.category.hasSuffix("_STICKER"), let stickerId = message.stickerId {
                
            } else {
                navigationController?.pushViewController(StickerAddViewController.instance(message: message), animated: true)
            }
        }
    }
}

// MARK: - UITableViewDelegate
extension ConversationViewController: UITableViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        updateAccessoryButtons(animated: !isAppearanceAnimating)
    }
    
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        if isShowingStickerPanel {
            toggleStickerPanel(delay: 0)
        }
        UIView.animate(withDuration: animationDuration) {
            self.updateHeaderViews(animated: false)
            self.dismissMenu(animated: false)
        }
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if !decelerate {
            updateHeaderViews(animated: true)
        }
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        guard !tableView.isTracking else {
            return
        }
        updateHeaderViews(animated: true)
    }
    
    func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
        tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
        return false
    }
    
    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        updateHeaderViews(animated: true)
    }
    
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        guard let dataSource = dataSource else {
            return
        }
        if indexPath.section == 0 && indexPath.row <= loadMoreMessageThreshold {
            dataSource.loadMoreAboveIfNeeded()
        }
        if let lastIndexPath = dataSource.lastIndexPath, indexPath.section == lastIndexPath.section, indexPath.row >= lastIndexPath.row - loadMoreMessageThreshold {
            dataSource.loadMoreBelowIfNeeded()
        }
        let messageId = dataSource.viewModel(for: indexPath)?.message.messageId
        if messageId == dataSource.firstUnreadMessageId || cell is UnreadHintMessageCell {
            unreadBadgeValue = 0
            dataSource.firstUnreadMessageId = nil
        }
        if let messageId = messageId, messageId == quotingMessageId {
            quotingMessageId = nil
        }
        if let viewModel = dataSource.viewModel(for: indexPath) as? AttachmentLoadingViewModel, viewModel.automaticallyLoadsAttachment {
            viewModel.beginAttachmentLoading()
        }
    }
    
    func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        guard let viewModel = dataSource?.viewModel(for: indexPath) else {
            return
        }
        if let viewModel = viewModel as? AttachmentLoadingViewModel, viewModel.automaticallyLoadsAttachment {
            viewModel.cancelAttachmentLoading(markMediaStatusCancelled: false)
        }
        if viewModel.message.messageId == quotingMessageId, let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last, lastVisibleIndexPath.section > indexPath.section || (lastVisibleIndexPath.section == indexPath.section && lastVisibleIndexPath.row > indexPath.row) {
            quotingMessageId = nil
        }
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        guard let viewModel = dataSource?.viewModel(for: indexPath) else {
            return 44
        }
        if viewModel.cellHeight.isNaN || viewModel.cellHeight < 1 {
            UIApplication.trackError("ConversationViewController", action: "Invalid row height", userInfo: ["viewModel": viewModel.debugDescription])
            return 44
        } else {
            return viewModel.cellHeight
        }
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return ConversationDateHeaderView.height
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: ConversationTableView.ReuseId.header.rawValue) as! ConversationDateHeaderView
        if let date = dataSource?.dates[section] {
            header.label.text = DateFormatter.yyyymmdd.date(from: date)?.timeDayAgo()
        }
        return header
    }
    
    func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        return nil
    }
    
}

// MARK: - DetailInfoMessageCellDelegate
extension ConversationViewController: DetailInfoMessageCellDelegate {
    
    func detailInfoMessageCellDidSelectFullname(_ cell: DetailInfoMessageCell) {


    }
    
}

// MARK: - AppButtonGroupMessageCellDelegate
extension ConversationViewController: AppButtonGroupMessageCellDelegate {
    
    func appButtonGroupMessageCell(_ cell: AppButtonGroupMessageCell, didSelectActionAt index: Int) {
        
        
    }
    
}

// MARK: - AttachmentLoadingMessageCellDelegate
extension ConversationViewController: AttachmentLoadingMessageCellDelegate {
    
    func attachmentLoadingCellDidSelectNetworkOperation(_ cell: MessageCell & AttachmentLoadingMessageCell) {
        guard let indexPath = tableView.indexPath(for: cell), let viewModel = dataSource?.viewModel(for: indexPath) as? MessageViewModel & AttachmentLoadingViewModel, let mediaStatus = viewModel.mediaStatus else {
            return
        }
        switch mediaStatus {
        case MediaStatus.CANCELED.rawValue:
            viewModel.beginAttachmentLoading()
        case MediaStatus.PENDING.rawValue:
            viewModel.cancelAttachmentLoading(markMediaStatusCancelled: true)
        default:
            break
        }
    }
    
}

// MARK: - CoreTextLabelDelegate
extension ConversationViewController: CoreTextLabelDelegate {
    
    func coreTextLabel(_ label: CoreTextLabel, didSelectURL url: URL) {
        open(url: url)
    }
    
    func coreTextLabel(_ label: CoreTextLabel, didLongPressOnURL url: URL) {
        let alert = UIAlertController(title: url.absoluteString, message: nil, preferredStyle: .actionSheet)
        alert.addAction(UIAlertAction(title: Localized.CHAT_MESSAGE_OPEN_URL, style: .default, handler: { [weak self](_) in
            self?.open(url: url)
        }))
        alert.addAction(UIAlertAction(title: Localized.CHAT_MESSAGE_MENU_COPY, style: .default, handler: { (_) in
            UIPasteboard.general.string = url.absoluteString
            NotificationCenter.default.postOnMain(name: .ToastMessageDidAppear, object: Localized.TOAST_COPIED)
        }))
        alert.addAction(UIAlertAction(title: Localized.DIALOG_BUTTON_CANCEL, style: .cancel, handler: nil))
        present(alert, animated: true, completion: nil)
    }
    
}

// MARK: - ImagePickerControllerDelegate
extension ConversationViewController: ImagePickerControllerDelegate {
    
    func imagePickerController(_ controller: ImagePickerController, didPickImage image: UIImage) {
        let previewViewController = AssetSendViewController.instance(image: image, dataSource: dataSource)
        navigationController?.pushViewController(previewViewController, animated: true)
    }
    
}

// MARK: - UIDocumentPickerDelegate
extension ConversationViewController: UIDocumentPickerDelegate {
    
    @available(iOS 11.0, *)
    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
        guard urls.count > 0 else {
            return
        }
        documentPicker(controller, didPickDocumentAt: urls[0])
    }
    
    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) {
        let previewViewController = FileSendViewController.instance(documentUrl: url, dataSource: dataSource)
        navigationController?.pushViewController(previewViewController, animated: true)
    }
    
}

// MARK: - UIDocumentInteractionControllerDelegate
extension ConversationViewController: UIDocumentInteractionControllerDelegate {
    
    func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
        return self
    }
    
    func documentInteractionControllerDidEndPreview(_ controller: UIDocumentInteractionController) {
        previewDocumentController = nil
    }
    
}

// MARK: - GalleryViewControllerDelegate
extension ConversationViewController: GalleryViewControllerDelegate {
    
    func galleryViewController(_ viewController: GalleryViewController, placeholderForItemOfMessageId id: String) -> UIImage? {
        if let indexPath = dataSource?.indexPath(where: { $0.messageId == id }), let cell = tableView.cellForRow(at: indexPath) as? PhotoRepresentableMessageCell {
            return cell.contentImageView.image
        } else {
            return nil
        }
    }
    
    func galleryViewController(_ viewController: GalleryViewController, sourceRectForItemOfMessageId id: String) -> CGRect? {
        if let indexPath = dataSource?.indexPath(where: { $0.messageId == id }), let cell = tableView.cellForRow(at: indexPath) as? PhotoRepresentableMessageCell {
            return cell.contentImageView.convert(cell.contentImageView.bounds, to: view)
        } else {
            return nil
        }
    }
    
    func galleryViewController(_ viewController: GalleryViewController, transition: GalleryViewController.Transition, stateDidChangeTo state: GalleryViewController.TransitionState, forItemOfMessageId id: String?) {
        var contentViews = [UIView]()
        if let indexPath = dataSource?.indexPath(where: { $0.messageId == id }) {
            let cell = tableView.cellForRow(at: indexPath)
            if let cell = cell as? PhotoRepresentableMessageCell {
                contentViews = [cell.contentImageView,
                                cell.shadowImageView,
                                cell.timeLabel,
                                cell.statusImageView]
            }
            if let cell = cell as? AttachmentExpirationHintingMessageCell {
                contentViews.append(cell.operationButton)
            }
            if let cell = cell as? VideoMessageCell {
                contentViews.append(cell.lengthLabel)
            }
        }
        switch state {
        case .began:
            contentViews.forEach {
                $0.isHidden = true
            }
        case .ended:
            if transition == .dismiss {
                view.sendSubview(toBack: galleryWrapperView)
            }
            fallthrough
        case .cancelled:
            contentViews.forEach {
                $0.isHidden = false
            }
        }
    }
    
    func galleryViewController(_ viewController: GalleryViewController, snapshotForItemOfMessageId id: String) -> UIView? {
        if let indexPath = dataSource?.indexPath(where: { $0.messageId == id }), let cell = tableView.cellForRow(at: indexPath) as? PhotoRepresentableMessageCell {
            return cell.contentSnapshotView(afterScreenUpdates: false)
        } else {
            return nil
        }
    }
    
    func animateAlongsideGalleryViewController(_ viewController: GalleryViewController, transition: GalleryViewController.Transition) {
        switch transition {
        case .show:
            hideStatusBar = true
        case .dismiss:
            hideStatusBar = false
        }
        setNeedsStatusBarAppearanceUpdate()
    }
    
}

// MARK: - PhotoAssetPickerDelegate
extension ConversationViewController: PhotoAssetPickerDelegate {

    func pickerController(_ picker: PickerViewController, contentOffset: CGPoint, didFinishPickingMediaWithAsset asset: PHAsset) {
        navigationController?.pushViewController(AssetSendViewController.instance(asset: asset, dataSource: dataSource), animated: true)
    }
    
}

// MARK: - UI Related Helpers
extension ConversationViewController {
    
    private func updateNavigationBar() {
        if let dataSource = dataSource, dataSource.category == .group {
            let conversation = dataSource.conversation
            titleLabel.text = conversation.name
            avatarImageView.setGroupImage(with: conversation.iconUrl, conversationId: conversation.conversationId)
        } else {
            dataSource.friendName
                .bind(to: titleLabel.rx.text)
                .disposed(by: disposeBag)
            
            dataSource.friendStatus
                .map { $0.toString() }
                .bind(to: participantsLabel.rx.text)
                .disposed(by: disposeBag)
        }
    }

    private func updateBottomView() {
        
    }
    
    private func updateMoreMenuFixedJobs() {
        moreMenuViewController?.fixedJobs = [.camera, .photo, .file]
    }
    
    private func updateMoreMenuApps() {
        if Thread.isMainThread {
            DispatchQueue.global().async { [weak self] in
                self?.updateMoreMenuApps()
            }
        } else {

        }
    }
    
    private func updateAccessoryButtons(animated: Bool) {
        let position = tableView.contentSize.height - tableView.contentOffset.y - tableView.bounds.height
        if scrollToBottomWrapperView.alpha < 0.1 && position > showScrollToBottomButtonThreshold {
            scrollToBottomWrapperHeightConstraint.constant = 48
            if animated {
                UIView.beginAnimations(nil, context: nil)
                UIView.setAnimationDuration(animationDuration)
            }
            scrollToBottomWrapperView.alpha = 1
            if animated {
                view.layoutIfNeeded()
                UIView.commitAnimations()
            }
        } else if scrollToBottomWrapperView.alpha > 0.9 && position < showScrollToBottomButtonThreshold {
            scrollToBottomWrapperHeightConstraint.constant = 4
            if animated {
                UIView.beginAnimations(nil, context: nil)
                UIView.setAnimationDuration(animationDuration)
            }
            scrollToBottomWrapperView.alpha = 0
            if animated {
                view.layoutIfNeeded()
                UIView.commitAnimations()
            }
            unreadBadgeValue = 0
            dataSource?.firstUnreadMessageId = nil
        }
    }
    
    private func dismissMenu(animated: Bool) {
        guard UIMenuController.shared.isMenuVisible else {
            return
        }
        UIMenuController.shared.setMenuVisible(false, animated: animated)
    }
    
    private func toggleStickerPanel(delay: TimeInterval) {
        stickerPanelContainerHeightConstraint.constant = lastKeyboardHeight
        inputWrapperBottomConstraint.constant = isShowingStickerPanel ? bottomSafeAreaInset : stickerPanelContainerHeightConstraint.constant
        let newAlpha: CGFloat = isShowingStickerPanel ? 0 : 1
        stickerKeyboardSwitcherButton.setImage(isShowingStickerPanel ? #imageLiteral(resourceName: "ic_chat_sticker") : #imageLiteral(resourceName: "ic_chat_keyboard"), for: .normal)
        sendButton.isHidden = !isShowingStickerPanel || !inputTextView.hasText
        toggleStickerPanelSizeButton.isHidden = isShowingStickerPanel
        isStickerPanelMax = false
        toggleStickerPanelSizeButton.setImage(#imageLiteral(resourceName: "ic_chat_panel_max"), for: .normal)
        let offset = inputWrapperBottomConstraint.constant - lastInputWrapperBottomConstant
        UIView.animate(withDuration: 0, delay: delay, options: [], animations: {
            UIView.setAnimationCurve(.overdamped)
            self.stickerPanelContainerView.alpha = newAlpha
            self.audioInputContainerView.isHidden = !self.isShowingStickerPanel
            if self.isShowingStickerPanel {
                self.dismissPanelsButton.alpha = 0
            }
            self.view.layoutIfNeeded()
            let contentOffsetY = self.tableView.contentOffset.y
            self.updateBottomInset()
            self.tableView.setContentOffsetYSafely(contentOffsetY + offset)
        }) { (_) in
            self.isShowingStickerPanel = !self.isShowingStickerPanel
            self.lastInputWrapperBottomConstant = self.inputWrapperBottomConstraint.constant
        }
    }
    
    private func updateHeaderViews(animated: Bool) {
        if animated {
            UIView.beginAnimations(nil, context: nil)
            UIView.setAnimationDuration(animationDuration)
        }
        var headerViews = tableView.headerViews
        if tableView.isTracking {
            headerViews.forEach {
                $0.contentAlpha = 1
            }
        } else {
            if let firstIndexPath = tableView.indexPathsForVisibleRows?.first,
                let firstCell = tableView.cellForRow(at: firstIndexPath),
                let headerView = tableView.headerView(forSection: firstIndexPath.section) as? ConversationDateHeaderView,
                headerView.frame.intersects(firstCell.frame) {
                if let index = headerViews.index(of: headerView) {
                    headerViews.remove(at: index)
                }
                headerView.contentAlpha = 0
            }
            headerViews.forEach {
                $0.contentAlpha = 1
            }
        }
        if animated {
            UIView.commitAnimations()
        }
    }
    
    private func updateStrangerTipsView() {
        
    }
    
    private func updateBottomInset() {
        let quotePreviewViewHeight = isShowingQuotePreviewView ? quotePreviewView.frame.height : 0
        tableView.contentInset.bottom = bottomOutsideWrapperView.frame.height + MessageViewModel.bottomSeparatorHeight + quotePreviewViewHeight
        tableView.scrollIndicatorInsets.bottom = bottomOutsideWrapperView.frame.height
    }
    
    private func setQuoteViewHidden(_ hidden: Bool) {
        if hidden {
            quoteMessageId = nil
        }
        quoteViewShowConstraint.priority = hidden ? .defaultLow : .defaultHigh
        quoteViewHiddenConstraint.priority = hidden ? .defaultHigh : .defaultLow
        inputTextViewLeadingShrinkConstraint.priority = hidden ? .almostRequired : .defaultLow
        inputTextViewLeadingExpandedConstraint.priority = hidden ? .defaultLow : .almostRequired
        audioInputContainerView.isHidden = !hidden
        stickerKeyboardSwitcherButton.isHidden = !hidden
        sendButton.isHidden = false
        UIView.animate(withDuration: animationDuration) {
            self.updateBottomInset()
            self.view.layoutIfNeeded()
        }
    }
    
    private func blinkCellBackground(at indexPath: IndexPath) {
        let animation = { (indexPath: IndexPath) in
            guard let cell = self.tableView.cellForRow(at: indexPath) as? DetailInfoMessageCell else {
                return
            }
            cell.updateAppearance(highlight: true, animated: false)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
                cell.updateAppearance(highlight: false, animated: true)
            })
        }
        if let visibleIndexPaths = tableView.indexPathsForVisibleRows, visibleIndexPaths.contains(indexPath) {
            animation(indexPath)
        } else {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
                animation(indexPath)
            })
        }
    }
    
}

// MARK: - Helpers
extension ConversationViewController {
    
    private var trimmedMessageDraft: String {
        return inputTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
    }
    
    private func saveDraft() {
        guard !conversationId.isEmpty else {
            return
        }
    }
    
    private func loadDraft() {
        
    }
    
    private func reloadParticipants() {
        
    }
    
    private func loadStickerAndAsset() {
        
    }
    
    private func openDocumentAction(message: MessageItem) {
        guard let mediaUrl = message.mediaUrl else {
            return
        }
        
        let url = URL(fileURLWithPath: mediaUrl)
        guard FileManager.default.fileExists(atPath: url.path)  else {
            UIApplication.trackError("ConversationViewController", action: "openDocumentAction file not exist")
            return
        }
        previewDocumentController = UIDocumentInteractionController(url: url)
        previewDocumentController?.delegate = self
        if !(previewDocumentController?.presentPreview(animated: true) ?? false) {
            previewDocumentController?.presentOpenInMenu(from: CGRect.zero, in: self.view, animated: true)
        }
    }

    private func showLoading() {
        loadingView.isHidden = false
        loadingView.startAnimating()
        titleStackView.isHidden = true
    }

    private func hideLoading() {
        loadingView.isHidden = true
        loadingView.stopAnimating()
        titleStackView.isHidden = false
    }
    
    private func open(url: URL) {

        guard !conversationId.isEmpty else {
            return
        }
        presentWebWindow(withURL: url)
    }
    
    private func presentWebWindow(withURL url: URL) {
        let window = WebWindow.instance(conversationId: conversationId)
        window.controller = self
        window.presentPopupControllerAnimated(url: url)
    }
    
}

// MARK: - Embedded classes
extension ConversationViewController {
    
    struct Position {
        let messageId: String
        let offset: CGFloat
    }
    
}
